查看原文
其他

Windows平台基于API Hook技术的WinInet网络库HttpDNS实现方案

蔡新建、耿涛 好未来技术 2023-03-15

背景:学而思网校直播课堂在线安装程序,是一个独立的应用程序,提供学生端的安装功能,为了减少安装包体积,避免引入第三方网络库,使用的是操作系统的WinInet网络库。为了更好的优化网络,提高网络连接的成功率,避免Local DNS造成的域名劫持等问题,采用HttpDNS方式实现域名解析。

01

为什么使用HttpDNS

相比于传统的DNS,HttpDNS主要有以下优势:   

1. 域名防劫持:

使用Http(Https)协议进行域名解析,域名解析请求直接发送至HttpDNS服务器,绕过运营商Local DNS,避免域名劫持问题。       

2. 调度精准:

由于运营商策略的多样性,其 Local DNS 的解析结果可能不是最近、最优的节点,HttpDNS能直接获取客户端 IP ,基于客户端 IP 获得最精准的解析结果,让客户端就近接入业务节点。

3. 实时生效 :

配合端上策略(热点域名预解析、缓存DNS解析结果、解析结果懒更新)实现毫秒级低解析延迟的域名解析效果。

02

HttpDNS实现方案

使用HttpDNS的通常方法有两个方案:        

方案一:发起网络请求之前把域名使用HttpDNS解析为IP地址,然后请求的时候把域名替换为IP进行请求,但是这种方案存在两个问题需要解决:        

1. 虚拟主机问题        

从http/1.1开始,header中支持Host字段,用来实现访问虚拟主机的目的。http请求header中必须配置适当的Host才能正确访问想要的服务,默认情况下Host字段是请求地址中的域名。如果直接把请求的域名替换成IP地址则无法正确访问对应服务,所以需要所使用的网络库支持自定义Host字段。而WinInet是Windows系统库,不支持修改Host字段。所以不能简单的把域名替换为解析后的IP发起请求。另外在https协议中,虚拟主机同时带来SNI问题,即在TLS握手阶段就需要指定适当的Host信息,以保证服务端可以返回正确的证书,否则会导致SSL握手失败。        

2. Https证书验证问题        

把域名直接替换为IP地址带来的另一个问题是SSL/TLS握手时候的证书验证问题。主要原因是服务端证书和客户端的peer name不一致导致的。一个简单的解决方案是忽略SSL证书验证失败这个问题,但是这样会导致https请求成了不安全的请求。  

方案二:如果第三方网络库提供域名解析的回调,可以自定义域名解析也可以实现HttpDNS。本文采用的就是这个方案,利用Windows的API Hook机制,对域名解析GetAddrInfoEx接口进行Hook,以实现自定义DNS解析,失败 情况下走默认DNS解析。

常用网络库提供的解决方案:

Qt5Network库:比如在qt 5.15版本中,connectToHostEncrypted这个接口,他提供了peer name参数来实现SSL握手阶段需要验证的peer name以解决证书验证域名不匹配的问题;

libcurl库:用curl_easy_setopt CURLOPT_RESO LVE提供自定义主机名到IP地址的解析,即可以自定义域名解析。

本文的解决方案:       

由于我们项目需要只能使用Windows系统的WinInet网络库,该库不支持修改Host头,也不提供域名解析的回调。但是Windows的域名解析一般使用的gethostbyname,GetAddrInfo,GetAddrInfoEx这些API来实现的,如果我们Hook这些API,来实现HttpDNS解析过程,如果失败了,再走默认的域名解析过程,这样就可以实现了HttpDNS功能了。

03

Windows Hook原理与实现

1. HOOK的分类

Hook分为应用层(Ring3)Hook和内核层(Ring0)Hook,应用层Hook适用于x86和x64,而内核层Hook一般仅在x86平台适用,因为从Windows Vista的64版本开始引入的Patch Guard技术极大地限制了Windows x64内核挂钩的使用。咱们的项目用的是注入Hook下面的Inline Hook,用微软detours库实现的Windows Hook。如图一所示:

图一

2. Inline Hook 的技术原理

内联Hook直接修改内存中的任意函数的代码,将其劫持至Hook API。它的适用范围更广,因为只要是内存中有的函数它都能Hook。        

Inline Hook的目标是系统函数,如下,图二是Hook之前的状态,procexp.exe进程调用ZwQuerySystemInformation()函数时,ZwQuery SystemInformation()的代码是正常的代码。图三是Hook后的状态,ZwQuerySystemInformation()函数开头5个字节已被修改,变成了jmp 0x10001120,也就是我们的注入代码的地址,之后便可以开始我们的自定义操作。0x1000116A我们先进行unhook操作(脱钩),目的是将ZwQuerySystemInformation()的代码恢复。大家可能有疑惑,为什么刚修改完又要恢复回来,原因很简单,Hook的目的是当调用某个函数时,我们能劫持进程的执行流。现在我们已经劫持了进程的执行流,便可以恢复ZwQuerySystemInformation()的代码,以便我们的注入代码可以正常调用ZwQuerySystemInformation()。执行完注入代码后,再次挂钩,监控该函数。

图二

图三

3. Inline Hook 的代码实现

// Code Hook函数BOOL hookByCode(LPCWSTR szDllName,LPCSTR szFuncName,PROC pfnNew,PBYTE pOrgBytes){ FARPROC pfnOrg { 0 }; DWORD dwOldProtect{ 0 }; DWORD dwAddress { 0 }; BYTE pBuf[5] { 0xE9,0, }; PBYTE pByte { nullptr }; // 获取ntdll.ZwQuerySystemInformation函数的地址 pfnOrg = (FARPROC)GetProcAddress(GetModuleHandle(szDllName), szFuncName); pByte = (PBYTE)pfnOrg; // 若已被钩取,则返回FALSE if (pByte[0] == 0xE9) { return FALSE; } // 为了修改5个字节,先向内存添加“写”属性 VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 备份原有代码(5字节) memcpy(pOrgBytes, pfnOrg, 5); // 计算JMP地址(相对偏移) dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5; memcpy(&pBuf[1], &dwAddress, 4); // “钩子”:修改5个字节(JMP XXXXXXXX) memcpy(pfnOrg, pBuf, 5); // 恢复内存属性 VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect); return TRUE;}

首先获取API的地址,并保存在pfnOrg中,然后修改内存段属性为RWX,备份原有代码(已便后续代码恢复),实时计算JMP的相对偏移,最后修改API前5字节的代码,恢复内存属性。

4、使用detours库实现Hook

detours库是微软提供的被广泛使用的用于API Hook的库,它封装了Hook的实现细节,使用起来非常方便。例如:GetAddrInfoEx是我们需要hook的API,声明Old_GetAddrInfoEx保留Hook之前的函数指针,New_GetAddrInfoEx为Hook后的函数指针,应用程序在适当的时机调用StartHook/StopHook以Hook对应的API。

INT (WSAAPI* Old_GetAddrInfoEx)( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle) = GetAddrInfoEx; INT WSAAPI New_GetAddrInfoEx( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle){ // 这里可以实现自己的dns解析逻辑 // ... // 自定义解析失败后,调用默认解析以兜底 return Old_GetAddrInfoEx(pName, pServiceName, dwNameSpace, lpNspId, hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle);}
bool StartHook(){ DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)Old_GetAddrInfoEx, New_GetAddrInfoEx); LONG ret = DetourTransactionCommit(); return ret == NO_ERROR;}
bool StopHook(){ DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)Old_GetAddrInfoEx, New_GetAddrInfoEx); LONG ret = DetourTransactionCommit(); return ret == NO_ERROR;}

04

Hook过程

WinInet网络请求的一般过程如下图四所示,在发送HttpSendRequest请求的时候会调用域名解析函数GetAddrInfoEx函数完成域名的解析。在域名解析的时候Hook GetAddrInfoEx函数。   Hook后的WinInet网络请求过程如下图五所示,在Hook  域名解析函数GetAddrInfoEx的时候成功以后,就不再调用原有的域名解析函数GetAddrInfoEx,而是调用自定义的域名解析函数。在调用自定义的域名解析函数失败的时候,有个兜底的策略,还调回原来的域名解析函数GetAddrInfoEx。下面是自定义的域名解析函数New_GetAddrInfoEx。

图四

图五

自定义域名解析函数如下所示:

// 从私有堆上分配ADDRINFOEX空间static void my_addressinfo_alloc( __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle){ ADDRINFOEX my_hints = *hints; my_hints.ai_family = AF_INET; my_hints.ai_flags ^= (AI_CANONNAME | AI_FQDN); Old_GetAddrInfoEx(L"localhost", pServiceName, dwNameSpace, lpNspId, &my_hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle);}
INT WSAAPI New_GetAddrInfoEx( __in_opt PCWSTR pName, __in_opt PCWSTR pServiceName, __in DWORD dwNameSpace, __in_opt LPGUID lpNspId, __in_opt const ADDRINFOEX* hints, __deref_out PADDRINFOEXW* ppResult, __in_opt struct timeval* timeout, __in_opt LPOVERLAPPED lpOverlapped, __in_opt LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine, __out_opt LPHANDLE lpHandle){ do { struct in_addr addr; // ip和localhost不需要httpdns if (pName == nullptr || hints == nullptr || InetPtonW(AF_INET, pName, (void*)&addr) || wcscmp(pName, L"localhost") == 0) { break; } // 从缓存或者云服务商获取该域名对应的ip列表 HttpDNS::IpList ipList = HttpDNS::instance()->getHostByName(pName); if (ipList.size() == 0) { break; } // 由于GetAddrInfoEx调用时候在私有堆上分配的内存,自己new的对象无法正常释放,会导致崩溃 // blog: http://www.youngroe.com/2018/12/01/Windows/windows_client_dns_over_https/ ADDRINFOEX* pTarget = nullptr; for (auto& ip : ipList) { // 私有堆上分配ADDRINFOEX空间 ADDRINFOEX* pTemp = nullptr; my_addressinfo_alloc(pServiceName, dwNameSpace, lpNspId, hints, &pTemp, timeout, lpOverlapped, lpCompletionRoutine, lpHandle); if (pTemp == nullptr) { continue; } if (*ppResult == nullptr) { *ppResult = pTemp; pTarget = *ppResult; } else { assert(pTarget); pTarget->ai_next = pTemp; pTarget = pTarget->ai_next; } std::string ipa = CStringUtil::wstring2string(ip); struct sockaddr_in* mysock = (struct sockaddr_in*)pTemp->ai_addr; mysock->sin_addr.S_un.S_addr = inet_addr(ipa.c_str()); } if (*ppResult == nullptr) { break; } return NO_ERROR; } while (false); return Old_GetAddrInfoEx(pName, pServiceName, dwNameSpace, lpNspId, hints, ppResult, timeout, lpOverlapped, lpCompletionRoutine, lpHandle);}

在Hook GetAddrInfoEx函数的实现过程中遇到了一个小问题,GetAddrInfoEx返回结果中的addrinfoexW内存分配问题。正常情况下返回结果中的addrinfoexW由GetAddrInfoEx函数在其私有堆上分配,然后调用者使用完结果后使用FreeAddrInfoEx 释放,但是当我们自己实现的时候很难获取到私有堆的句柄,这样就没办法为addrinfoexW分配内存,如果使用new分配内存会在FreeAddrInfoEx 释放时错误产生问题。我实现的时候通过一个简单粗暴的方式是通过调用原始的GetAddrInfoEx解析localhost然后直接使用结果中的addrinfoexW,因为是GetAddrInfoEx分配,所以最后使用FreeAddrInfoEx 释放也没问题。

注意问题:

  • 我们只实现了IPv4的HttpDNS

  • 过滤掉localhost这样的解析

  • 过滤掉非域名类解析

05

总结

优点:使用 API Hook技术的WinInet网络库HttpDNS 可以起到降级的作用, 省去了DNS解析的一个全流程;使用这种技术对业务层是全透明的,不需要对业务层代码进行任何修改。

缺点:Hook  GetAddrInfoEx函数的时候增加了多余的localhost的解析,有一定的性能影响。

06

参考文献


  • HTTPS(含SNI)业务场景“IP直连”方案说明

    https://help.aliyun.com/document_detail/30143.html

  • HTTPS IP直连问题小结

    https://blog.csdn.net/leelit/article/details/77829196

  • Windows客户端如何透明使用DNS-over-HTTPS

    http://www.youngroe.com/2018/12/01/Windows/windows_client_dns_over_https/

  • TLS SNI问题

    https://www.jianshu.com/p/f608611dc694

  • 微软官方API HOOK库

    https://github.com/microsoft/Detours

  • Windows Hook原理与实现

    https://blog.csdn.net/m0_37552052/article/details/81453591


扫描下方二维码添加加「好未来技术」微信官方账号

进入好未来技术官方交流群与作者实时互动~

(若扫码无效,可通过微信号TAL-111111直接添加)

- 也许你还想看 -

世界读书日|好未来技术免费送书啦!

好未来数据中台|中台全域特征池能力初探

Java并发编程-知识前瞻(第一章)


我知道你“在看”哟~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存